Error Handling for REST
Spring에는 여러가지 Exception Handling 방식이 있다. 하지만 REST API에서는 또 다른 방식의 예외 처리 방식이 있는데 그걸 알아보려고 한다.
간략한 역사
- Spring 3.2 이전: Spring MVC Application에서는
HandlerExceptionResolver나@ExceptionHandlerAnnotation을 이용하는 것이 대표적인 방법이였다. 하지만 단점도 존재했다. - Spring 3.2 이후:
@ControllerAdvice가 등장하였고, 위의 두 방법의 단점을 해결하였고, 애플리케이션 전체에서 통일된 예외처리가 가능해졌다. - Spring 5: REST API를 위한
ResponseStatusException클래스가 등장하였다.
1번째: @ExceptionHandler
Exception Handler에서 설명했듯이, @ExceptionHandler를 @Controller 가 붙은 컨트롤러의 메서드에 붙여 예외를 처리하는 것이다.
다만 해당 컨트롤러에서만 예외처리를 할 수 있다.
@Controller
public class ExceptionController{
@ExceptionHandler({IllegalArgumentException.class})
public ResponseEntity<String> handle(final Exception exception) {
return ResponseEntity.badRequest().body(exception.getMessage());
}
}
여기서 handle은 오로지 ExceptionController 범위 안에서만 IllegalArgumentException을 잡아낼 수 있다.
만약 @ControllerAdvice 어노테이션이 붙은 클래스에서 사용하면 전역적으로 예외 처리가 가능하다.
2번째: ResponseStatus를 사용한 예외 처리
Custom Exception에 @ResponseStatus를 붙이거나 ResponseStatusException 예외를 발생시켜서 예외처리가 가능하다.
Custom Exception에 @ResponseStatus 붙여주기
DispactherServlet에는 ResponseStatusExceptionResolver가 기본으로 활성화되어있는데, 이 리졸버가 처리한다.
https://www.baeldung.com/spring-response-status#controller
When we want to signal an error, we can provide an error message via the reason argument:
@ResponseStatus(HttpStatus.BAD_REQUEST, reason = "Some parameters are invalid")
void onIllegalArgumentException(IllegalArgumentException exception) {}
Note, that when we set reason, Spring calls HttpServletResponse.sendError(). Therefore, it will send an HTML error page to the client, which makes it a bad fit for REST endpoints. Also note, that Spring only uses
@ResponseStatus, when the marked method completes successfully (without throwing anException).
TL;DR: ResponseStatus는 HttpServletResponse.sendError()를 호출하기 때문에 REST endpoint에는 맞지 않는다.
ResponseStatusException 예외 발생시키기
https://www.baeldung.com/spring-response-status-exception
Spring 5 이후에 등장한 예외로, REST하게 예외를 발생시킬 수 있도록 한다.
@ResponseStatus와 마찬가지로 ResponseStatusExceptionResolver가 처리한다.
3가지의 생성자가 있다:
ResponseStatusException(HttpStatus status)
ResponseStatusException(HttpStatus status, java.lang.String reason)
ResponseStatusException(
HttpStatus status,
java.lang.String reason,
java.lang.Throwable cause
)
- status: HTTP Status
- reason: 예외 메세지
- cause: 예외가 발생된 원인 예외
사용 예시는 아래와 같다.
@GetMapping("/actor/{id}")
public String getActorName(@PathVariable("id") int id) {
try {
return actorService.getActor(id);
} catch (ActorNotFoundException ex) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Actor Not Found", ex);
}
}
응답은 아래와 같이 온다.
$ curl -i -s -X GET http://localhost:8081/actor/8
HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Dec 2020 19:38:09 GMT
{
"timestamp": "2020-12-26T19:38:09.426+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/actor/8"
}
장점으로는
- 같은 예외 타입이라도 다른 상태와 다른 메세지를 지정해서 응답을 보낼 수 있다. → 결합도를 낮춘다.
- 불필요한 예외 타입을 추가로 만들지 않아도 된다.
예외 처리 우선 순위
| Resolver | Matching | Priority |
|---|---|---|
| ExceptionHandlerExceptionResolver | @ExceptionHandler → custom exception handling | 1 |
| ResponseStatusExceptionResolver | @ResponseStatus → HTTP status code & ResponseStatusException | 2 |
| DefaultHandlerExceptionResolver | exceptions from Spring → HTTP Status code | 3 |